package org.easiermock;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;

import net.sf.cglib.core.CollectionUtils;
import net.sf.cglib.core.VisibilityPredicate;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Enhancer;

import org.objenesis.Objenesis;
import org.objenesis.ObjenesisStd;

/**
 * Provides factory methods for creating EasierMock objects.   
 * 
 * @author Neil Fuller
 */
public class EasierMock {
	
	private static Objenesis objenesis = new ObjenesisStd();
	
	private EasierMock() {
		super();
	}
	
	/**
	 * Creates a mock object that implements/extends the specified class
	 * and delegates through to the supplied implementation object if 
	 * it provides compatible methods. Methods are considered compatible if
	 * they have the same signature and are annotated with the 
	 * <code>Mock</code> annotation (either directly or if the class the 
	 * method is declared on is annotated).
	 * 
	 * <p>An optional set of MethodListeners can be provided that will be 
	 * called prior to the calls to mocked methods.
	 * 
	 * @param <T> the class of the mock to create (usually inferred from the toMockClass parameter)
	 * @param toMockClass the class to mock
	 * @param mockImpl the mock implementation. Does not have to implement the <code>toMockClass</code> but the 
	 * 		signatures of the methods must be compatible to be  
	 * @param methodListeners
	 * @return
	 */
	public static <T> T createMock(Class<T> toMockClass, Object mockImpl, MethodListener ... methodListeners) {
		DefaultMockContext mockContext = new DefaultMockContext();
		for (MethodListener methodListener : methodListeners) {
			mockContext.addMethodListener(methodListener);
		}
		return createMock(toMockClass, mockImpl, mockContext);
	}
	
	static <T> T createMock(Class<T> toMockClass, Object mockImpl, MockContext mockContext) {
		DelegatingInvocationHandler invocationHandler = new DelegatingInvocationHandler(mockImpl, mockContext);
		return createMock(toMockClass, mockImpl, invocationHandler);
	}
	
	@SuppressWarnings("unchecked")
	private static <T> T createMock(Class<T> toMockClass, Object mockImpl, InvocationHandler invocationHandler) {
		T mock;
		if (toMockClass.isInterface()) {
			ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
			mock = (T)Proxy.newProxyInstance(classLoader, new Class[] { toMockClass }, invocationHandler);
		} else {
			Callback interceptor = new CglibInvocationHandlerAdapter(invocationHandler);
			// BEGIN: CODE LARGELY COPIED FROM EASYMOCK
			
	        // Create the mock
	        Enhancer enhancer = new Enhancer() {
	            /**
	             * Filter all private constructors but do not check that there are
	             * some left
	             */
	        	@Override
	            protected void filterConstructors(Class sc, List constructors) {
	                CollectionUtils.filter(constructors, new VisibilityPredicate(
	                        sc, true));
	            }
	        };
	        enhancer.setSuperclass(toMockClass);
	        enhancer.setCallbackType(interceptor.getClass());

	        Class<?> mockClass = enhancer.createClass();
	        Enhancer.registerCallbacks(mockClass, new Callback[] { interceptor });

			// END: CODE LARGELY COPIED FROM EASYMOCK
	        
	        mock = (T)objenesis.newInstance(mockClass);
		}
		return mock;
	}
	
	private static class CglibInvocationHandlerAdapter implements net.sf.cglib.proxy.InvocationHandler {

		private InvocationHandler delegate;
		
		public CglibInvocationHandlerAdapter(InvocationHandler delegate) {
			super();
			this.delegate = delegate;
		}

		public Object invoke(Object proxy, Method method, Object[] args)
				throws Throwable {
			return delegate.invoke(proxy, method, args);
		}
		
	}
}
